package drds.common.db.xml_mapping.binding;

import drds.common.db.xml_mapping.reflection.ExceptionUtil;
import drds.common.db.xml_mapping.session.MapperContext;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

public class MapperProxy<T> implements InvocationHandler {
  private final MapperContext mapperContext;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodToMapperMethodMap;

  public MapperProxy(MapperContext mapperContext, Class<T> mapperInterface, Map<Method, MapperMethod> methodToMapperMethodMap) {
    this.mapperContext = mapperContext;
    this.mapperInterface = mapperInterface;
    this.methodToMapperMethodMap = methodToMapperMethodMap;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {

      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(mapperContext, args);
  }

  private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
    throws Throwable {
    final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
      .getDeclaredConstructor(Class.class, int.class);
    if (!constructor.isAccessible()) {
      constructor.setAccessible(true);
    }
    final Class<?> declaringClass = method.getDeclaringClass();
    return constructor
      .newInstance(declaringClass,
        MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
          | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
      .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    return methodToMapperMethodMap.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, mapperContext.getConfiguration()));
  }


}
